/*********************************
 * jsmud: JavaScript MUD Kernel  *
 *          Mojodo, Inc.         *
 *        Sean Middleditch       *
 * Copyright (C) 2005,2007,2008  *
 *      All rights reserved.     *
 *   See COPYING for licensing.  *
 *********************************/

/* ==== SYSTEM INCLUDES ==== */

#include <string.h>
#include <errno.h>
#include <limits.h>
#include <arpa/telnet.h>
#include <sys/stat.h>

/* ==== LIBRARY INCLUDES ==== */

#include <sqlite3.h>
#include <zlib.h>

#define XP_UNIX 1
#define MOZILLA_1_8_BRANCH 1
#include <jsapi.h>
#undef XP_UNIX
#undef MOZILLA_1_8_BRANCH

/* ==== JSMUD HEADER ==== */

#include "jsmud.h"

/* ==== SCRIPT BINDINGS ==== */

/* -- GLOBAL -- */

static JSBool js_log (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_error (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_getSetting (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_require (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_exit (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);

static JSFunctionSpec js_global_functions[] = {
	{"require", js_require, 1, 0, 0},
	{"log", js_log, 1, 0, 0},
	{"error", js_error, 1, 0, 0},
	{"getSetting", js_getSetting, 2, 0, 0},
	{"exit", js_exit, 0, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_log (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	log_msg("%s", JS_GetStringBytes(string));

	return JS_TRUE;
}

static JSBool js_error (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	log_error("%s", JS_GetStringBytes(string));

	return JS_TRUE;
}

static JSBool js_getSetting (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;
	const char* name;
	Setting* setting;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	name = JS_GetStringBytes(string);

	for (setting = opt_custom; setting != NULL; setting = setting->next) {
		if (!strcmp(setting->name, name)) {
			string = JS_NewStringCopyZ(cx, setting->value);
			if (string == NULL)
				return JS_FALSE;
			*rval = STRING_TO_JSVAL(string);
			return JS_TRUE;
		}
	}

	*rval = argv[1];

	return JS_TRUE;
}

static JSBool js_require (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;
	const char* path;
	const char* real_path;
	JSScript* script;

	/* convert path to a string (store reference in argv for gc-safety) */
	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	argv[0] = STRING_TO_JSVAL(string);
	path = JS_GetStringBytes(string);

	/* check that path is safe/legal to use */
	if ((real_path = js_path(cx, "js", path)) == NULL)
		return JS_FALSE;

	if ((script = JS_CompileFile(cx, JS_GetGlobalObject(cx), real_path)) == NULL)
		return JS_FALSE;
	if (!JS_ExecuteScript(cx, JS_GetGlobalObject(cx), script, rval))
		return JS_FALSE;

	return JS_TRUE;
}

static JSBool js_exit (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	exit_flag = 1;
	return JS_TRUE;
}

/* -- NAMESPACE: Net -- */

static JSBool js_net_listen (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_net_registerzmp (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_net_haszmp (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);

static JSFunctionSpec js_net_functions[] = {
	{"listen", js_net_listen, 3, 0, 0},
	{"registerZMP", js_net_registerzmp, 2, 0, 0},
	{"hasZMP", js_net_haszmp, 1, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_net_listen (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;
	const char* interface;
	int32 port;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	interface = JS_GetStringBytes(string);

	if (!JS_ValueToInt32(cx, argv[1], &port))
		return JS_FALSE;

	if (JS_ValueToFunction(cx, argv[2]) == NULL)
		return JS_FALSE;

	if (listen_new(interface, port, &argv[2]) == NULL)
		return JS_FALSE;

	return JS_TRUE;
}

static JSBool js_net_registerzmp (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;

	if (JS_ValueToFunction(cx, argv[1]) == NULL)
		return JS_FALSE;

	if (zmp_register(JS_GetStringBytes(string), &argv[1]) == NULL)
		return JS_FALSE;

	return JS_TRUE;
}

static JSBool js_net_haszmp (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;

	if (zmp_has(JS_GetStringBytes(string)))
		*rval = BOOLEAN_TO_JSVAL(JS_TRUE);
	else
		*rval = BOOLEAN_TO_JSVAL(JS_FALSE);

	return JS_TRUE;
}

/* -- NAMESPACE: Timer -- */

static JSBool js_timer_register (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_timer_setscale (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);

static JSFunctionSpec js_timer_functions[] = {
	{"register", js_timer_register, 3, 0, 0},
	{"setScale", js_timer_setscale, 1, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_timer_register (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	double time;
	JSBool gametime;

	if (JS_ValueToNumber(cx, argv[0], &time) == JS_FALSE)
		return JS_FALSE;

	if (JS_ValueToFunction(cx, argv[1]) == NULL)
		return JS_FALSE;

	if (JS_ValueToBoolean(cx, argv[2], &gametime) == JS_FALSE)
		return JS_FALSE;

	if (timer_new(time, gametime, &argv[1]) == NULL)
		return JS_FALSE;

	return JS_TRUE;
}

static JSBool js_timer_setscale (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	double scale;

	if (JS_ValueToNumber(cx, argv[0], &scale) == JS_FALSE)
		return JS_FALSE;

	if (scale < 0.0)
		return JS_FALSE;

	timer_game_scale = scale;

	return JS_TRUE;
}

/* -- NAMESPACE: DB -- */

static JSBool js_db_exec (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_db_query (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);

static JSFunctionSpec js_db_functions[] = {
	{"exec", js_db_exec, 1, 0, 0},
	{"query", js_db_query, 1, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_db_exec (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;
	const char* sql;
	sqlite3_stmt* stmt;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	sql = JS_GetStringBytes(string);

	stmt = db_prepare(sql, argc - 1, &argv[1]);
	if (stmt == NULL)
		return JS_FALSE;

	sqlite3_step(stmt);
	sqlite3_finalize(stmt);

	return JS_TRUE;
}

static JSBool js_db_query (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	JSString* string;
	sqlite3_stmt* stmt;
	JSObject* result;
	const char* sql;

	if ((string = JS_ValueToString(cx, argv[0])) == NULL)
		return JS_FALSE;
	sql = JS_GetStringBytes(string);

	if ((result = JS_NewObject(cx, &js_resultset_class, js_resultset_prototype, NULL)) == NULL)
		return JS_FALSE;
	*rval = OBJECT_TO_JSVAL(result);

	stmt = db_prepare(sql, argc - 1, &argv[1]);
	if (stmt == NULL)
		return JS_FALSE;

	JS_SetPrivate(cx, result, stmt);

	return JS_TRUE;
}

/* -- NAMESPACE: File */

static JSBool js_file_get (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_file_put (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);

static JSFunctionSpec js_file_functions[] = {
	{"get", js_file_get, 1, 0, 0},
	{"put", js_file_put, 2, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_file_get (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	const char* path;
	const char* real_path;
	char buf[4096];
	size_t rs;
	FILE* file;

	/* check args -- get fiel name */
	if (!JS_ConvertArguments(cx, argc, argv, "s", &path))
		return JS_FALSE;

	/* convert to real path */
	if ((real_path = js_path(cx, "txt", path)) == NULL)
		return JS_FALSE;

	/* open the file */
	if ((file = fopen(real_path, "r")) == NULL) {
		js_raise(cx, "Failed to open file `%s': %s", path, strerror(errno));
		return JS_FALSE;
	}

	/* create result string; make this growable for performance */
	*rval = STRING_TO_JSVAL(JS_NewGrowableString(cx, 0, 0));

	/* read in the file.  it sucks there is no API append function; we have
	 * to create a string for each buffer read, then append it to our growable
	 * result string */
	while (!feof(file)) {
		rs = fread(buf, 1, sizeof(buf), file);
		if (rs > 0)
			*rval = STRING_TO_JSVAL(JS_ConcatStrings(cx, JSVAL_TO_STRING(*rval), JS_NewStringCopyN(cx, buf, rs)));
	}

	/* close the file and make our growable string a regular string */
	fclose(file);
	JS_MakeStringImmutable(cx, JSVAL_TO_STRING(*rval));

	return JS_TRUE;
}

static JSBool js_file_put (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	const char* path;
	const char* contents;
	const char* real_path;
	size_t rs;
	size_t len;
	FILE* file;

	/* check args -- get fiel name */
	if (!JS_ConvertArguments(cx, argc, argv, "ss", &path, &contents))
		return JS_FALSE;

	/* convert to real path */
	if ((real_path = js_path(cx, "txt", path)) == NULL)
		return JS_FALSE;

	/* open the file */
	if ((file = fopen(real_path, "w")) == NULL) {
		js_raise(cx, "Failed to open file `%s': %s", path, strerror(errno));
		return JS_FALSE;
	}

	/* write out the file */
	len = strlen(contents);
	while (!feof(file) && len > 0) {
		rs = fwrite(contents, 1, len, file);
		contents += rs;
		len -= rs;
	}

	/* close the file and make our growable string a regular string */
	fclose(file);

	return JS_TRUE;
}

/* -- CLASS: ResultSet */

static JSBool js_resultset_next (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static void js_resultset_finalize (JSContext* cx, JSObject* self);

JSClass js_resultset_class = {
	"ResultSet", JSCLASS_HAS_PRIVATE,
	JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,
	JS_EnumerateStub,JS_ResolveStub,JS_ConvertStub,js_resultset_finalize,
	NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL
};
JSObject* js_resultset_prototype;

static JSFunctionSpec js_resultset_methods[] = {
	{"next", js_resultset_next, 1, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_resultset_next (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	sqlite3_stmt* stmt;

	stmt = (sqlite3_stmt*)JS_GetPrivate(cx, self);
	if (stmt == NULL) {
		*rval = JSVAL_NULL;
		return JS_TRUE;
	}

	if (db_get_result(stmt, rval) == JS_FALSE) {
		*rval = JSVAL_NULL;
		sqlite3_finalize(stmt);
		JS_SetPrivate(cx, self, NULL);
		return JS_TRUE;
	}

	return JS_TRUE;
}

static void js_resultset_finalize (JSContext* cx, JSObject* self)
{
	sqlite3_stmt* stmt = (sqlite3_stmt*)JS_GetInstancePrivate(cx, self, &js_resultset_class, NULL);
	if (stmt != NULL)
		sqlite3_finalize(stmt);
}

/* -- CLASS: Connection -- */

static JSBool js_connection_send (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_connection_sendZMP (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_connection_setEcho (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static JSBool js_connection_disconnect (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval);
static void js_connection_finalize (JSContext* cx, JSObject* self);

JSClass js_connection_class = {
	"Connection", JSCLASS_HAS_PRIVATE,
	JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,
	JS_EnumerateStub,JS_ResolveStub,JS_ConvertStub,js_connection_finalize,
	NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL
};
JSObject* js_connection_prototype;

static JSFunctionSpec js_connection_methods[] = {
	{"send", js_connection_send, 1, 0, 0},
	{"sendZMP", js_connection_sendZMP, 1, 0, 0},
	{"setEcho", js_connection_setEcho, 1, 0, 0},
	{"disconnect", js_connection_disconnect, 0, 0, 0},
	{NULL, 0, 0, 0, 0}
};

static JSBool js_connection_send (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	Client* client;
	char* text;

	if (!JS_ConvertArguments(cx, argc, argv, "s", &text))
		return JS_FALSE;

	if ((client = JS_GetInstancePrivate(cx, self, &js_connection_class, NULL)) == NULL)
		return JS_FALSE;

	client_send_text(client, text, strlen(text));
	
	return JS_TRUE;
}

static JSBool js_connection_sendZMP (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	Client* client;
	int i;
	const char* args[TELNET_ZMP_MAX_ARGS];

	if (!JS_ConvertArguments(cx, argc, argv, "s", &args[0]))
		return JS_FALSE;

	if (argc > TELNET_ZMP_MAX_ARGS) {
		JS_ReportError(cx, "sendZMP only supports %d arguments", TELNET_ZMP_MAX_ARGS);
		return JS_FALSE;
	}

	if ((client = JS_GetInstancePrivate(cx, self, &js_connection_class, NULL)) == NULL)
		return JS_FALSE;

	for (i = 1; i < argc; ++i) {
		if (!(argv[i] = STRING_TO_JSVAL(JS_ValueToString(cx, argv[i]))))
			return JS_FALSE;
	}

	client_send_zmp(client, args[0], i-1, &args[1]);
	
	return JS_TRUE;
}

static JSBool js_connection_setEcho (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	Client* client;
	JSBool value;

	if ((client = JS_GetInstancePrivate(cx, self, &js_connection_class, NULL)) == NULL)
		return JS_FALSE;

	if (JS_ValueToBoolean(cx, argv[0], &value) == JS_FALSE)
		return JS_FALSE;

	if (value) {
		client->flags |= TF_WANT_ECHO;
		client_send_iac (client, WONT, TELOPT_ECHO);
	} else {
		client->flags &= ~TF_WANT_ECHO;
		client_send_iac (client, WILL, TELOPT_ECHO);
	}

	return JS_TRUE;
}

static JSBool js_connection_disconnect (JSContext* cx, JSObject* self, uintN argc, jsval* argv, jsval* rval)
{
	Client* client;

	if ((client = JS_GetInstancePrivate(cx, self, &js_connection_class, NULL)) == NULL)
		return JS_FALSE;

	client_disconnect(client);

	return JS_TRUE;
}

static void js_connection_finalize (JSContext* cx, JSObject* self)
{
	Client* client;
	if ((client = JS_GetInstancePrivate(cx, self, &js_connection_class, NULL)) != NULL)
		client_disconnect(client);
}

/* ==== INITIALIZE BINDINGS ==== */

int init_javascript_bindings (void)
{
	JSObject* ns_net;
	JSObject* ns_db;
	JSObject* ns_timer;
	JSObject* ns_file;

	/* create "namespace" object */
	if ((ns_net = JS_DefineObject(js_context, js_global, "Net", NULL, NULL, 0)) == NULL) {
		log_error("JS_DefineObject() failed");
		return 0;
	}
	if ((ns_db = JS_DefineObject(js_context, js_global, "DB", NULL, NULL, 0)) == NULL) {
		log_error("JS_DefineObject() failed");
		return 0;
	}
	if ((ns_timer = JS_DefineObject(js_context, js_global, "Timer", NULL, NULL, 0)) == NULL) {
		log_error("JS_DefineObject() failed");
		return 0;
	}
	if ((ns_file = JS_DefineObject(js_context, js_global, "File", NULL, NULL, 0)) == NULL) {
		log_error("JS_DefineObject() failed");
		return 0;
	}

	/* create all our custom functions */
	if (!JS_DefineFunctions(js_context, js_global, js_global_functions)) {
		log_error("JS_DefineFunctions() failed");
		return 0;
	}
	if (!JS_DefineFunctions(js_context, ns_net, js_net_functions)) {
		log_error("JS_DefineFunctions() failed");
		return 0;
	}
	if (!JS_DefineFunctions(js_context, ns_db, js_db_functions)) {
		log_error("JS_DefineFunctions() failed");
		return 0;
	}
	if (!JS_DefineFunctions(js_context, ns_timer, js_timer_functions)) {
		log_error("JS_DefineFunctions() failed");
		return 0;
	}
	if (!JS_DefineFunctions(js_context, ns_file, js_file_functions)) {
		log_error("JS_DefineFunctions() failed");
		return 0;
	}

	/* create our database result set class */
	if ((js_resultset_prototype = JS_InitClass(js_context, ns_db, NULL, &js_resultset_class, NULL, 0, NULL, js_resultset_methods, NULL, NULL)) == NULL) {
		log_error("JS_InitClass() failed");
		return 0;
	}

	/* create our telnet client class */
	if ((js_connection_prototype = JS_InitClass(js_context, ns_net, NULL, &js_connection_class, NULL, 0, NULL, js_connection_methods, NULL, NULL)) == NULL) {
		log_error("JS_InitClass() failed");
		return 0;
	}

	return 1;
}

/* vim: set tabstop=2 shiftwidth=2 ai: */
